/*
XPTracker - Alternative Agile Tracking Tool
Copyright (C) 2006-2009  Stephen Starkey

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package org.xptracker.domain;

import org.apache.commons.lang.StringUtils;
import org.hibernate.annotations.*;
import org.xptracker.persistence.ExtIdAssigned;
import org.xptracker.persistence.Identified;
import org.xptracker.persistence.LastModifiedTracker;
import org.xptracker.util.TimeUtil;
import org.xptracker.search.SearchedItem;

import javax.persistence.*;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.CascadeType;
import java.util.*;

@Entity
@Table(name="stories", uniqueConstraints = @UniqueConstraint(columnNames = "ext_id"))
@Cache(usage= CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Proxy(lazy=false)
@SearchedItem(idField="id")
public class Story extends Identified implements LastModifiedTracker, ExtIdAssigned {
  private Long extId;
  private double estimate;
  private String summary;
  private String description;
  private Date createdDate = new Date();
  private Date lastModified = createdDate;
  private Date completedDate;
  private Date dueDate;
  private User creator;
  private Severity severity = Severity.warm;
  private Size size = Size.medium;
  private State state = State.dreaming;
  private User owner;
  private Queue queue;
  private WikiContent descriptionContent = new WikiContent();
  private List<StoryCategory> categories = new LinkedList<StoryCategory>();
  private List<EffortEntry> effortEntries = new LinkedList<EffortEntry>();

  private Set<User> trackers = new LinkedHashSet<User>();
  private Set<Customer> customers = new LinkedHashSet<Customer>();
  private List<StoryTask> tasks = initializeTasks();
  private List<BlogEntry> blogEntries = new LinkedList<BlogEntry>();
  private List<FileAttachment> fileAttachments;

  @Column(name="ext_id")
  @SearchField(queryField ="id", stored=true, description="Story ID")
  public Long getExtId() {
    return extId;
  }

  public void setExtId(Long extId) {
    this.extId = extId;
  }

  public double getEstimate() {
    return estimate;
  }

  public void setEstimate(double estimate) {
    this.estimate = estimate;
  }

  @SearchField(queryField ="summary", description = "Story summary")
  public String getSummary() {
    return summary;
  }

  public void setSummary(String summary) {
    this.summary = summary;
  }

  @Lob
  @Column(length=65535, name="description")
  @Deprecated
  public String getOldDescription() {
    return description;
  }

  @Deprecated
  public void setOldDescription(String description) {
    this.description = description;
  }

  @SearchField(queryField ="description", description = "Story description")
  @Transient
  public String getDescription() {
    final WikiContent content = getDescriptionContent();
    return content == null ? "" : content.getContent();
  }

  @Transient
  public void setDescription(String description) {
    final WikiContent content = getDescriptionContent();
    if (content == null) {
      throw new RuntimeException("WikiContent should not be null!");
    }
    content.setContent(description);
  }

  @SearchField(queryField ="created", description = "Date the story was created", format="yyyyMMdd")
  @Column(name="created_date")
  public Date getCreatedDate() {
    return createdDate;
  }

  public void setCreatedDate(Date createdDate) {
    this.createdDate = createdDate;
  }

  @SearchField(queryField ="completed", description = "Date the story was completed", format="yyyyMMdd")
  @Column(name="completed_date")
  public Date getCompletedDate() {
    return completedDate;
  }

  public void setCompletedDate(Date completedDate) {
    this.completedDate = completedDate;
  }

  @Column(name="due_date")
  @SearchField(queryField="dueDate", description="Story Due Date")
  public Date getDueDate() {
    return dueDate;
  }

  public void setDueDate(Date dueDate) {
    this.dueDate = dueDate;
  }

  @SearchField(queryField ="creator", description = "User who created the story")
  @ManyToOne(targetEntity=User.class)
  public User getCreator() {
    return creator;
  }

  public void setCreator(User creator) {
    this.creator = creator;
  }

  @SearchField(queryField = "severity", description = "Severity of the story",
      format = "cold, tepid, warm, hot, scorcher")
  @Enumerated(EnumType.STRING)
  public Severity getSeverity() {
    return severity;
  }

  public void setSeverity(Severity severity) {
    this.severity = severity;
  }

  @SearchField(queryField = "size", description = "Size of the story", format="petit, small, medium, large, xl")
  @Enumerated(EnumType.STRING)
  public Size getSize() {
    if (size == null) {
      size = Size.medium;
    }
    return size;
  }

  public void setSize(Size size) {
    this.size = size;
  }

  @SearchField(queryField ="state", description = "State of the story",
      format="dreaming, inqueue, inprocess, blocked, developed, tested, released, rejected")
  @Enumerated(EnumType.STRING)
  public State getState() {
    return state;
  }

  public void setState(State state) {
    this.state = state;
  }

  @SearchField(queryField ="owner", description = "Username or full name of the owner of the story")
  @ManyToOne
  public User getOwner() {
    return owner;
  }

  protected void setOwner(User owner) {
    this.owner = owner;
  }

  public void setOwnerForMigration(User owner) {
    setOwner(owner);
  }

  public void changeOwner(User owner) {
    if (owner == null) {
      throw new IllegalArgumentException("Owner may not be null.  Use removeOwner() instead.");
    }
    this.owner = owner;
    addTracker(owner);
  }

  public void removeOwner() {
    this.owner = null;
  }

  @ManyToOne(cascade = CascadeType.ALL)
  @JoinColumn(name="description_content_id")
  public WikiContent getDescriptionContent() {
    if (descriptionContent == null) {
      descriptionContent = new WikiContent();
    }
    return descriptionContent;
  }

  public void setDescriptionContent(WikiContent descriptionContent) {
    this.descriptionContent = descriptionContent;
  }

  @SearchField(queryField ="customers", description = "Any of the customers in the story")
  @ManyToMany(fetch = FetchType.EAGER)
  @JoinTable(name="story_customers", joinColumns = @JoinColumn(name="story_id"),
        inverseJoinColumns = @JoinColumn(name="customer_id"))
  public Set<Customer> getCustomers() {
    return customers;
  }

  public void setCustomers(Set<Customer> customers) {
    this.customers = customers;
  }

  public void addCustomer(Customer customer) {
    if (customer == null) {
      throw new IllegalArgumentException("Customer to add may not be null.");
    }
    customers.add(customer);
  }

  public boolean removeCustomer(Customer customer) {
    return customers.remove(customer);
  }

  @SearchField(queryField = "tasks", description = "Any of the text of any tasks in the story")
  @OneToMany()
  @IndexColumn(name = "task_idx")
  @JoinTable(name="story_task_mapping", joinColumns = @JoinColumn(name="story_id"),
        inverseJoinColumns = @JoinColumn(name="task_id"))
  public List<StoryTask> getTasks() {
    return tasks;
  }

  public void setTasks(List<StoryTask> tasks) {
    this.tasks = tasks;
  }

  public void removeTask(StoryTask storyTask) {
    List<StoryTask> newTasks = initializeTasks();
    List<StoryTask> oldTasks = getTasks();
    oldTasks.remove(storyTask);
    newTasks.addAll(oldTasks);
    setTasks(newTasks);
  }

  private LinkedList<StoryTask> initializeTasks() {
    return new LinkedList<StoryTask>();
  }

  @SearchField(queryField ="categories", description = "Any category in the story")
  @ManyToMany(fetch=FetchType.EAGER)
  @JoinTable(name="story_categories_mapping", joinColumns = @JoinColumn(name="story_id"),
        inverseJoinColumns = @JoinColumn(name="category_id"))
  @IndexColumn(name="category_idx")
  @Fetch(FetchMode.JOIN)
  public List<StoryCategory> getCategories() {
    return categories;
  }

  public void setCategories(List<StoryCategory> categories) {
    this.categories = categories;
  }

  @SearchField(queryField ="blog", description = "Any text in all the blog entries of the story")
  @OneToMany()
  @IndexColumn(name="entry_idx")
  @JoinTable(name="story_blog_entries_mapping", joinColumns = @JoinColumn(name="story_id"),
        inverseJoinColumns = @JoinColumn(name="entry_id"))
  public List<BlogEntry> getBlogEntries() {
    return blogEntries;
  }

  public void setBlogEntries(List<BlogEntry> blogEntries) {
    this.blogEntries = blogEntries;
  }

  @SearchField(queryField ="queue", description = "The name of the queue the story is in")
  @ManyToOne(targetEntity=Queue.class)
  public Queue getQueue() {
    return queue;
  }

  public void setQueue(Queue queue) {
    this.queue = queue;
  }

  @SearchField(queryField ="effort", description = "The amount of effort in the story")
  @OneToMany(fetch=FetchType.EAGER)
  @JoinTable(name="story_effort_entries", joinColumns = @JoinColumn(name="story_id"),
        inverseJoinColumns = @JoinColumn(name="entry_id"))
  @IndexColumn(name="entry_idx")
  @Fetch(FetchMode.SUBSELECT)
  public List<EffortEntry> getEffortEntries() {
    return effortEntries;
  }

  public void setEffortEntries(List<EffortEntry> effortEntries) {
    this.effortEntries = effortEntries;
  }

  @Transient
  public long getTotalEffort() {
    long totalEffort = 0l;
    for (EffortEntry entry : effortEntries) {
      totalEffort += entry.getDuration();
    }
    return totalEffort;
  }

  @Transient
  public double getHoursRemaining() {
    return getEstimate() - TimeUtil.fromMillis(getTotalEffort(), "hours");
  }

  @Override
  public String toString() {
    return "Story " + getExtId() + ": '" + summary + "'";
  }

  @SearchField(queryField ="lastModified", description = "The date the story was last modified", format="yyyyMMdd")
  @Column(name="modified_date")
  public Date getLastModified() {
    return lastModified;
  }

  public void setLastModified(Date lastModified) {
    this.lastModified = lastModified;
  }

  @SearchField(queryField ="trackers", description = "Username or full name of any of the trackers for the story")
  @ManyToMany()
  @JoinTable(name="story_trackers", joinColumns = @JoinColumn(name="story_id"),
        inverseJoinColumns = @JoinColumn(name="user_id"))
  public Set<User> getTrackers() {
    return trackers;
  }

  public void setTrackers(Set<User> trackers) {
    this.trackers = trackers;
  }

  public boolean isUserTrackingStory(User user) {
    for(User tracker: trackers) {
      if (tracker.equals(user)) {
        return true;
      }
    }
    return false;
  }

  public void addTracker(User user) {
    if (user == null) {
      throw new IllegalArgumentException("User to add as tracker may not be null.");
    }
    trackers.add(user);
  }

  public boolean removeTracker(User user) throws IllegalStateException {
    if(owner != null && owner.equals(user)) {
      throw new IllegalStateException("Can not remove tracker, since user owns story: " + this + " user:" + owner);
    }
    return trackers.remove(user);
  }

  public boolean canRemoveTracker(User user) {
     return owner == null || (!owner.equals(user) && this.isUserTrackingStory(user));
  }

  public FileAttachment addFileAttachment(String description, String filename, String contentType, byte[] contents) {
    initAttachments();
    FileAttachment attachment = new FileAttachment(description, filename, contentType, contents);
    fileAttachments.add(attachment);
    return attachment;
  }

  @OneToMany()
  @JoinTable(name="story_file_attachments", joinColumns = @JoinColumn(name="story_id"),
        inverseJoinColumns = @JoinColumn(name="attachment_id"))
  @IndexColumn(name="attachment_idx")
  public List<FileAttachment> getFileAttachments() {
    initAttachments();
    return fileAttachments;
  }

  public void setFileAttachments(List<FileAttachment> fileAttachments) {
    this.fileAttachments = fileAttachments;
  }

  private void initAttachments() {
    if (fileAttachments == null) {
      fileAttachments = new LinkedList<FileAttachment>();
    }
  }

  public FileAttachment removeAttachment(String id) {
    List<FileAttachment> oldAttachments = fileAttachments;
    fileAttachments = null;
    initAttachments();

    FileAttachment removed = null;
    for(FileAttachment attachment: oldAttachments) {
      if (StringUtils.equals(attachment.getId(), id)) {
        if (removed != null) {
          throw new IllegalStateException("ID matched multiple attachments!");
        }
        removed = attachment;
      } else {
        fileAttachments.add(attachment);
      }
    }
    return removed;
  }

  @Transient
  public boolean isDeveloped() {
    return State.developed.equals(getState());
  }

  @Transient
  public boolean isTested() {
    return State.tested.equals(getState());
  }

  @Transient
  public boolean isReleased() {
    return State.released.equals(getState());
  }
}
